C++23 新特性
C++23 是一个 minor 版本,但含金量不低:std::expected、std::print、std::generator、flat 容器、多维 operator[]、「推导 this」(deducing this)都直接改变日常写法。它还把 C++20 协程和范围这两座「只给了机制没给工具」的框架补上了实用的库工具。
编译器支持情况见 Compiler support for C++23。系列总览见 现代 C++ 新特性汇总。
后续版本:C++26 已于 2025 年中完成特性冻结与委员会审定(反射、契约、
std::execution等进入标准),正式文本预计 2026 年内发布。
语言特性
新语言功能特性测试宏
每个新语言特性都有对应的 __cpp_XXX 宏(语言侧),可精确探测编译器支持:
#if __cpp_explicit_this_parameter >= 202110L
// 推导 this 可用
#endif
#if __has_cpp_attribute(assume) // 属性类的用 __has_cpp_attribute 探测
// ...
#endif 显式对象形参(推导 this)
成员函数的第一个参数可以显式写 this(this 参数),类型由编译器按调用方式推导——一个签名同时覆盖 const/非 const、左值/右值四种版本:
struct Box {
int v{};
// 传统写法需要两份几乎相同的代码:
// int& value() { return v; }
// const int& value() const { return v; }
// 推导 this:一份代码覆盖全部版本
template<typename Self>
auto&& value(this Self&& self) {
return std::forward_like<Self>(self.v); // self 是 Box& / const Box& / Box&& ...
}
};
Box b;
const Box cb;
b.value() = 1; // OK:int&
int x = cb.value(); // OK:const int& 另一个重要应用:递归 lambda(不再需要 std::function 自我引用的技巧):
auto factorial = [](this auto self, int n) -> long long {
return n <= 1 ? 1 : n * self(n - 1); // self 就是 lambda 自己
};
factorial(10); // 3628800 原理:显式对象形参让 this 成为普通函数参数,按模板推导规则得到 Self = Box& 或 const Box&,成员访问通过 self. 隐式完成。
if consteval / if not consteval
编译期/运行期走不同实现,取代容易用错的 std::is_constant_evaluated():
constexpr double fast_inv_sqrt(double x) {
if consteval { // 编译期求值中:只允许纯计算
double y = x;
for (int i = 0; i < 80; ++i)
y = y * (1.5 - 0.5 * x * y * y); // 牛顿迭代
return y;
} else { // 运行期:直接用硬件平方根
return 1.0 / std::sqrt(x);
}
} 为什么需要专门语法:if constexpr (std::is_constant_evaluated()) 是错的——在 if constexpr 的条件里求值,永远处于常量求值上下文,条件恒为 true,运行期分支被整个剪掉。if consteval 才能正确区分两种环境。
多维下标运算符
operator[] 可以接受多个参数,多维访问不再需要 m[i][j] 或 at(i, j):
struct Matrix {
double m[3][3]{};
double& operator[](std::size_t r, std::size_t c) { // 多维 operator[]
return m[r][c];
}
};
Matrix mat;
mat[1, 2] = 3.14; // 直接按行列访问
// 注意坑:对没有多维 operator[] 的容器,v[1, 3] 走的仍是逗号运算符
// std::vector<int> v{0,1,2,3};
// v[1, 3] 等价 v[3] ——(下标里逗号表达式的行为已随之标记为弃用) 配套的库设施是 std::mdspan(见标准库部分)。
static operator()
函数调用运算符可以是 static 的:无 this 指针、无状态:
struct Plus {
static constexpr int operator()(int a, int b) { // 没有 this
return a + b;
}
};
Plus p;
p(2, 3); // 对象语法仍可用
std::accumulate(v.begin(), v.end(), 0, Plus{}); // 当谓词/运算器直接用 意义:比非静态版本少传一个 this;无捕获 lambda 生成的正是这类闭包类型,显式写出后,与要求「可默认构造」的泛型代码(C++20 concepts 的 default_initializable)配合更顺畅。
static operator[]
同理,下标运算符也可以 static,用于与对象状态无关的索引场景(查全局表、纯函数映射):
struct SineTable {
// 表是编译期全局数据,operator[] 不需要对象状态
static constexpr double operator[](std::size_t i) {
return table[i]; // 假设 table 是静态常量
}
static inline double table[360]{ /* ... */ };
};
double s = SineTable{}[30]; // 无需构造有状态对象 auto(x):语言中的衰退复制
auto(x) 显式制造一份「衰退拷贝」(decay copy):去引用、去顶层 const、数组退化成指针,得到值类型的临时副本:
std::string name = "task-data";
// 异步任务要一份自己的拷贝,写法一目了然:
std::thread t([msg = auto(name)] { // 明确:按值拷贝一份,而非引用
process(msg);
});
int x = 42;
int& r = x;
auto a = auto(r); // int:不再是引用(auto a = r; 得到的也是 int,但意图不明显)
// auto(x) 的类型 = std::decay_t<decltype(x)>
// 花括号形式同样有效:auto{x} 与 T(x) 的区别:不需要知道 T 是什么,「把这个东西按值复制一份」有了统一写法;标准库内部(thread 参数、ranges)大量使用这个语义。
lambda 表达式上的属性
属性可以出现在 lambda 形参列表之后,作用于调用运算符本身:
auto f = [](int x) [[nodiscard]] { return x * 2; }; // 丢弃返回值会告警
auto g = []() [[noreturn]] { throw std::runtime_error("fatal"); };
// 以前 [[nodiscard]] 无法套在 lambda 上,只能套在具名函数上 可选的扩展浮点类型
<stdfloat> 提供定宽浮点类型(实现可选,硬件不支持可不提供):
#include <stdfloat>
std::float16_t h; // IEEE 半精度 16 位:GPU、深度学习推理
std::float32_t f; // 通常与 float 同表示(可能是别名)
std::float64_t d; // 通常与 double 同表示
std::float128_t q; // 四精度:高精度科学计算
std::bfloat16_t bf; // bfloat16:范围同 float、精度低,训练场景主流 动机:机器学习与 GPU 生态里 half/bfloat16 早已事实标准化,C++ 里却只能靠编译器扩展(__fp16、_Float16),现在统一进标准。
std::size_t 字面量的后缀 uz / z
uz/UZ 后缀直接得到 std::size_t 类型的字面量,消除循环里的有符号/无符号比较告警:
std::vector<int> v(100);
for (auto i = 0uz; i < v.size(); ++i) // i 是 size_t,无符号比较告警消失
v[i] = static_cast<int>(i);
// v[42uz] 这类下标写法也不再产生转换
// z 是有符号对应物(ptrdiff_t 类),uz 是无符号 size_t
auto n = 42z;
static_assert(std::is_signed_v<decltype(n)>); #elifdef、#elifndef 与 #warning
预处理指令补全:
#if defined(NDEBUG)
// release 路径
#elifndef NDEBUG
// 没定义 NDEBUG 的调试路径
#endif
#ifndef FEATURE_X
# warning "FEATURE_X not defined; falling back to defaults"
#endif
// 以前 #elif 只能写 defined(X),嵌套 #if 很啰嗦;#warning 提示构建配置问题 新属性 [[assume(表达式)]]
告诉编译器「此处该表达式恒为真」,不做求值、不生成代码,仅供优化:
int fast_div(int a, int n) {
[[assume(n > 0)]]; // 不求值;编译器据此可以:
return a / n; // 省略除零/负数除法的防御分支、按正数优化
}
// 代价:若运行时假设不成立 -> 未定义行为,自己负责
// 与 assert 相反:assert 是"检查",assume 是"保证" 具名通用字符转义
字符转义可以按码点名或带花括号的十六进制书写:
char alpha = '\N{GREEK SMALL LETTER ALPHA}'; // 具名转义(Unicode 标准名)
char zhong = '\u{4E2D}'; // 花括号十六进制码点:'中'
// 好处:\u{...} 不再受"4 位还是 8 位"的写法歧义困扰,
// \N{...} 自带文档含义,比裸码点可读 可移植源文件编码为 UTF-8
C++23 起源文件的编码必须是 UTF-8(不再由实现自定义),跨平台编译含中文注释/字符串的代码不再因源码编码出问题(老 MSVC 默认 GBK 的年代结束)。
行拼合之前修剪空白
反斜杠续行时,反斜杠后面的尾随空白/注释不再破坏拼合:
constexpr double total = 1.0 + 2.0 \
+ 3.0;
// 以前:行尾反斜杠后若有空格(部分编译器)续行失效;现在一律先修剪再拼合 标准库特性
新的库功能特性测试宏
库侧特性宏集中在 <version>,__cpp_lib_XXX 形式:
#include <version>
#ifdef __cpp_lib_expected
# include <expected> // 有 std::expected 才用
#endif
#ifdef __cpp_lib_print
std::println("hello");
#endif 新的范围折叠算法
std::ranges::fold_left 一族把折叠操作纳入范围算法:
#include <algorithm>
int sum = std::ranges::fold_left(v, 0, std::plus{}); // ((0+v0)+v1)+v2
int prod = std::ranges::fold_left(v, 1, std::multiplies{});
std::string cat = std::ranges::fold_left(words, std::string{}, std::plus{});
// 家族成员:
// fold_right:从右往左
// fold_left_first / fold_right_last:用首/末元素当初值,空范围返回 optional
// fold_left_with_result:折叠同时带回最终迭代器位置 对比手写循环:折叠意图显式,配合投影可写 fold_left(people, 0, std::plus{}, &Person::age)。
字符串格式化改进
std::format 体系三处升级(其中 print 是日常影响最大的):
// 1. std::print / std::println:直接输出,告别 iostream 拼接
std::print("{} items, total {:.2f}\n", n, total);
std::println("done"); // 自动换行
std::print(stderr, "error: {}", code); // 指定文件流
// 2. 指针的调试格式:任何指针都能打印(0x 前缀地址),调试日志方便
int* p = &x;
std::println("p = {}", p); // p = 0x7ffd8c3b1a4c(无需转 uintptr_t)
// 3. 范围格式化:容器直接可打印
std::vector<int> v{1, 2, 3};
std::println("{}", v); // [1, 2, 3]
std::map<std::string, int> m{{"a", 1}};
std::println("{}", m); // {"a": 1}(格式细节实现略有差异) 「平铺」容器适配器
flat_map / flat_set 等:接口与 map/set 一致,底层是排序的连续数组:
#include <flat_map>
std::flat_map<int, std::string> m; // 底层默认是两个排序 vector
m[3] = "c";
m[1] = "a";
m[2] = "b"; // 查找:O(log n) 二分 + 连续内存,常数远小于红黑树
for (auto it = m.lower_bound(1); it != m.upper_bound(2); ++it)
std::println("{}:{}", it->first, it->second); // 有序遍历,行为同 map | 对比项 | map | flat_map |
|---|---|---|
| 查找 | O(log n),指针跳转 | O(log n) 二分,缓存友好(实际更快) |
| 插入/删除 | O(log n) | O(n) 挪动数组 |
| 内存 | 每节点 3 指针+颜色开销 | 紧凑连续 |
| 迭代器稳定性 | 插入不失效 | 插入可能全失效 |
结论:读多写少、批量构建后只查的配置表/查找表,flat_map 几乎总是更快。
std::mdspan
多维数组的非拥有视图(「多维版 span」),配合多维 operator[]:
#include <mdspan>
std::vector<int> buf(12);
std::mdspan mat(buf.data(), 3, 4); // 3 行 4 列的视图,不拷贝数据
mat[2, 3] = 42; // 多维下标,C++23 新语法
mat[0, 0] = 1;
// 定长多维:std::mdspan<int, std::extents<std::size_t, 3, 4>> 固定 3x4
// 还可选布局(layout_right 行主 / layout_left 列主)——接 BLAS、图像、深度学习张量 价值:把「一维 buffer + 手算下标」的裸代码变成类型安全的多维视图,零拷贝、零分配。
std::generator
协程的官方易用封装:std::generator<T> 就是一个可以 co_yield 的惰性序列:
#include <generator>
std::generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
a = std::exchange(b, a + b);
}
}
for (int x : fibonacci() | std::views::take(10))
std::print("{} ", x); // 0 1 1 2 3 5 8 13 21 34
// 生成器按需产出一个消费一个,无限序列也不爆内存
// C++20 里手写的 60 行 Generator 模板,从此一行 using 换 std::generator string 与 string_view 的 contains
std::string s = "hello.cpp";
std::string_view sv = s;
s.contains(".cpp"); // true:子串判断
sv.contains("hello"); // true
s.contains('c'); // 单字符重载
// 以前要写 s.find(x) != std::string::npos,可读性差且容易写反 禁止从 nullptr 构造 string_view
std::string_view 的 nullptr 构造被删除,从源头堵住未定义行为:
std::string_view sv = nullptr; // C++23 起编译错误
// 以前:构造出 data()==nullptr 的 view,一旦读取就是 UB
// 修复思路:想要空 view 用默认构造或 "",想检查空指针用指针判空 resize_and_overwrite
先改大小再由你接管写入,跳过无意义的零填充:
std::string s;
s.resize_and_overwrite(64, [](char* p, std::size_t n) {
// p 指向可用缓冲,n 是请求的容量:直接写入,返回实际保留长度
auto res = std::format_to_n(p, n - 1, "{} items", 42);
return static_cast<std::size_t>(res.size);
});
// 传统 resize(64) 会先填 64 个 '\0' 再被覆盖——纯浪费;序列化/读文件场景收益明显 std::optional 的单子式操作
or_else / and_then / transform 把「层层判空」变成管道:
std::optional<std::string> label =
find_user(id) // optional<User>
.and_then(user_email) // -> optional<string>,找不到用户则保持空
.transform([](auto s) { return s + "@corp"; })
.or_else([] { return std::optional{"unknown@corp"}; }); 规则:and_then 的回调返回 optional(链式展平),transform 的回调返回普通值(自动包一层),or_else 在空时触发。std::expected(见下)同样有这一套。
栈踪迹(stacktrace)库
<stacktrace> 拿调用栈,崩溃/诊断日志不再依赖平台 API:
#include <stacktrace>
void on_error() {
std::cout << std::stacktrace::current() << '\n'; // 当前调用栈
// 可只取最近几帧:std::stacktrace::current(1, 5)
}
// 也可以存起来慢慢用
auto trace = std::stacktrace::current();
if (trace) { // 至少有一帧
for (const auto& frame : trace)
std::cout << frame << '\n'; // 函数名、文件、行号
} 注意:需要调试信息(-g)才有函数名/行号;libstdc++ 需要额外链接(GCC 14+ 为 -lstdc++exp,更早版本为 -lstdc++_libbacktrace)。
新的范围算法
bool has = std::ranges::contains(v, 42); // 是否包含
auto it = std::ranges::find_last(v, 7); // 最后一个匹配
std::ranges::iota(v, 0); // 填充 0,1,2,...(算法版)
// std::ranges::generate_random(v, engine, dist); // 向范围直接填充随机数
// 还有 fold 一族(单列一节)、find_last_if、shift 等 新的范围适配器(视图)
// enumerate:带下标遍历(不再需要手动维护下标变量)
for (auto [i, x] : v | std::views::enumerate)
std::println("[{}] {}", i, x);
// zip:并行遍历多个序列
for (auto [name, score] : std::views::zip(names, scores))
std::println("{}: {}", name, score);
// chunk / slide:分块与滑窗
for (auto&& c : v | std::views::chunk(3)) { /* 每 3 个一组 */ }
for (auto&& w : v | std::views::slide(2)) { /* 相邻对 (v[i], v[i+1]) */ }
// 常用的还有:
// stride:跳步采样 cartesian_product:笛卡尔积
// join_with:展开并插入分隔 chunk_by:按谓词分组
// repeat:重复序列 as_rvalue:把元素按右值送入管道 对范围库的修改与对视图的修改
ranges::to:把惰性管道物化回容器(写 pipeline 的最后一环):cppauto out = v | std::views::filter(even) | std::views::transform(square) | std::ranges::to<std::vector>(); // 物化成 vector auto s = words | std::views::join_with(' ') | std::ranges::to<std::string>(); // 也能拼字符串视图可以拥有元素(从右值容器构造时接管数据,不再悬垂),管道中传右值容器是安全的
适配器支持用
|直接接在「容器生成表达式」后,成套组合性增强(views 侧的配套微调)
std::unreachable
标记「编译器认为到不了、真到了就是 UB」的代码位置:
#include <utility>
int to_code(Color c) {
switch (c) {
case Color::red: return 1;
case Color::green: return 2;
case Color::blue: return 3;
}
std::unreachable(); // 到这里 = UB;好处:
// 1. 编译器知道 switch 已穷尽,省掉"落到 default"的分支
// 2. 比注释 /* unreachable */ 有真实语义,也比 assert(false) 少生成代码
// 3. UB 检测工具(UBSan)在误入时会立刻报告
} std::expected
expected<T, E>:「要么是值 T,要么是错误 E」的返回类型,给异常受限环境一个正经的错误传递通道:
#include <expected>
enum class ParseError { empty, not_number };
std::expected<int, ParseError> parse(std::string_view sv) noexcept {
if (sv.empty()) return std::unexpected{ParseError::empty};
// ... 解析
return 42;
}
auto r = parse("42");
if (r) // 有值
use(*r);
else
handle(r.error()); // r.error() 是 ParseError
// 与 optional 同款单子操作,链式编排不层层 if:
auto doubled = parse("21")
.transform([](int x) { return x * 2; }) // 值存在才映射
.value_or(0); // 出错给 0
// 对比:用 optional 丢掉了"错在哪"的信息;用异常破坏 noexcept 与实时性
// expected 是两者之间的第三条路 std::move_only_function
std::function 的可移动版:只要求可调用物可移动(std::function 要求可拷贝),所以能装 lambda 捕获的 unique_ptr:
#include <functional>
std::move_only_function<int()> task =
[cfg = std::make_unique<Config>()] {
return cfg->run();
};
// task 不可拷贝,但可以移动——正好是线程池任务的形态:
std::vector<std::move_only_function<void()>> queue;
queue.push_back([buf = std::move(owned_buffer)] { process(buf); });
// 出队执行时:auto t = std::move(queue.front()); t(); 它还支持在类型里标注调用约定(move_only_function<int() const noexcept>),比 std::function 更精确。
std::spanstream
以 span<char> 为缓冲的流:零分配的 ostream/istream:
#include <spanstream>
char buf[64];
// 输出:写进自己的缓冲,不碰堆
std::ospanstream os{std::span<char>{buf}};
os << "value = " << 42 << " (0x" << std::hex << 42 << ")";
auto written = os.span().size(); // 实际写入字节数
// 输入:把内存当流解析
std::ispanstream is{std::span<const char>{buf, written}};
std::string tag; int n;
is >> tag >> n; // "value" 42
// 嵌入式/日志/协议场景:要"流的格式化能力"又不能分配内存 std::byteswap
字节序翻转,constexpr 可用:
#include <bit>
constexpr auto host = std::uint32_t{0x11223344};
constexpr auto be = std::byteswap(host); // 0x44332211
// 网络字节序、跨平台文件格式解析:
std::uint32_t to_network(std::uint32_t v) {
if constexpr (std::endian::native == std::endian::little)
return std::byteswap(v); // 小端机器翻一下
return v;
}
// 注意:byteswap 只是翻转,不感知"网络序";配合 std::endian(C++20)判断 std::to_underlying
枚举转底层整数,不再手写 static_cast<int>:
enum class Status : std::uint8_t { ok, retry, drop };
auto raw = std::to_underlying(Status::retry); // 1,类型是底层类型 uint8_t
// 以前:static_cast<std::underlying_type_t<Status>>(s) 一长串
// 序列化、打日志、喂 C API 时干净多了 关联容器的异质擦除
map/set 的 erase 支持异构 key(与 C++14 的异构查找、C++20 的异构 insert 构成闭环):
std::map<std::string, int, std::less<>> m;
m.erase(std::string_view{"stale"}); // 不构造临时 std::string
// 以前:m.erase(std::string("stale")) 必须先造一个 string
// 同样要求透明比较器(less<>) 小结
C++23 的主线是「补齐易用性」:std::print 与格式化增强直接改善输出体验,expected 给错误处理提供了异常与 optional 之外的第三条路,generator/ranges::to/flat 容器把 C++20 框架的最后一公里走完,推导 this 消灭了成对的 const/非 const 成员函数样板。工具链方面,GCC 13+/Clang 17+ 已覆盖大部分库特性,按特性测试宏渐进采用即可。
系列其他篇目:现代 C++ 新特性汇总 · C++11 新特性 · C++14 新特性 · C++17 新特性 · C++20 新特性